// Copyright (c) 2010 http://xbeeservice.codeplex.com
// This source is subject to the Microsoft Public License.
// See http://www.opensource.org/licenses/ms-pl.html
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.Ccr.Core;
using Microsoft.Dss.Core;
using Microsoft.Dss.Core.Attributes;
using Microsoft.Dss.Core.DsspHttp;
using Microsoft.Dss.ServiceModel.Dssp;
using Microsoft.Dss.ServiceModel.DsspServiceBase;
using submgr = Microsoft.Dss.Services.SubscriptionManager;

namespace SoCGadget.Robotics.XBee
{
    /// <summary>
    /// Microsoft Robotics Studio service used for communicating with an XBee device connected to the PC.
    /// </summary>
    [Contract(Contract.Identifier)]
    [DisplayName( "SoCGadget XBeeService" )]
    [Description( "Provides access to remote XBee devices" )]
    class XBeeService : DsspServiceBase
    {
        // Default state
        private const int Baud = 9600;
        private const string ApiMode = "ApiOperationEscaped";

        private const int MinBaudRate = 300;

        /// <summary>
        /// Our abstraction on the Grommet XBee Module
        /// </summary>
        private XBeeServiceImpl _controller;

        [EmbeddedResource( "SoCGadget.Robotics.XBee.XBeeServiceState.xslt" )]
        private string _transform = null;
        
        /// <summary>
        /// Service state
        /// </summary>
        [ServiceState( StateTransform = "SoCGadget.Robotics.XBee.XBeeServiceState.xslt" )]
        [InitialStatePartner( Optional = true, ServiceUri = ServicePaths.Store + "/SocGadget.XBeeServiceState.xml" )]
        private XBeeServiceState _state = new XBeeServiceState();
		
        /// <summary>
        /// Main service port
        /// </summary>
        [ServicePort("/XBeeService", AllowMultipleInstances = false)]
#pragma warning disable 169
        private XBeeServiceOperations _mainPort = new XBeeServiceOperations();
#pragma warning restore 169
		
        [SubscriptionManagerPartner]
        private submgr.SubscriptionManagerPort _submgrPort = new submgr.SubscriptionManagerPort();

        private EventHandler<DataSampleReceivedArgs> _dataSampleReceivedHandler;

        /// <summary>
        /// Service constructor
        /// </summary>
        public XBeeService(DsspServiceCreationPort creationPort)
            : base(creationPort)
        {
            _dataSampleReceivedHandler = _controller_DataSampleReceived;
        }
		
        /// <summary>
        /// Service start
        /// </summary>
        protected override void Start()
        {
            if ( _state == null )
            {
                // No saved state, create new state and set in some defaults
                _state = new XBeeServiceState
                {
                    Baud = Baud,
                    ApiMode = ApiMode
                };
            }

            base.Start();
        }

        #region Service handlers

        /// <summary>
        /// Get Handler.
        /// </summary>
        /// <param name="get"></param>
        /// <returns></returns>
        [ServiceHandler( ServiceHandlerBehavior.Concurrent )]
        public virtual IEnumerator<ITask> GetHandler( Get get )
        {
            get.ResponsePort.Post( _state );
            yield break;
        }

        /// <summary>
        /// Http Get Handler
        /// </summary>
        [ServiceHandler( ServiceHandlerBehavior.Concurrent )]
        public IEnumerator<ITask> HttpGetHandler( HttpGet httpGet )
        {
            httpGet.ResponsePort.Post( new HttpResponseType( System.Net.HttpStatusCode.OK, _state, _transform ) );
            yield break;
        }

        /// <summary>
        /// Connects to an XBee device on a COM port. If a serial port was not specified by the caller
        /// nor was one found in the unitial state loaded by the service, a search will be performed to find the device.
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [ServiceHandler( ServiceHandlerBehavior.Exclusive )]
        public IEnumerator<ITask> ConnectHandler( Connect request )
        {
            XBeeServiceState state = new XBeeServiceState();
            state.Baud = request.Body.Baud < MinBaudRate ? _state.Baud : request.Body.Baud;
            state.ApiMode = string.IsNullOrEmpty( request.Body.ApiMode ) ? _state.ApiMode : request.Body.ApiMode;
            state.SerialPort = string.IsNullOrEmpty( request.Body.SerialPort ) ? _state.SerialPort : request.Body.SerialPort;

            // Already connect? Ignore multiple connect requests and just return
            if ( _controller != null && _controller.IsConnected )
            {
                request.ResponsePort.Post( new DefaultUpdateResponseType() );
                yield break;
            }

            try
            {
                // SerialPort could come from two places 1) specified in the connect call or 2) from deserialzied service state.
                // If we still don't have a serial port, OR the port we have does not have an XBee on it, search for one.
                if ( string.IsNullOrEmpty( state.SerialPort )
                    || !XBeeServiceImpl.TestXBeeDevice( state.SerialPort, state.Baud, state.ApiMode ) )
                {
                    // throw an exception if we cannot find an xbee controller
                    state.SerialPort = XBeeServiceImpl.FindXBeeDevice( state.Baud, state.ApiMode );
                    if ( string.IsNullOrEmpty( state.SerialPort ) )
                        throw new Exception( "No XBee device found." );
                }

                _controller = new XBeeServiceImpl( new GrommetXBeeModule( state.SerialPort, state.Baud, state.ApiMode ) );
                _controller.DataSampleReceived += _dataSampleReceivedHandler;

                // Save the state
                _state = state;
                SaveState( _state );
            }
            catch ( Exception ex )
            {
                SoapFault fault = new SoapFault( "Error from XBee service.", ex );
                LogError( fault.ToString() );
                request.ResponsePort.Post( fault );
                _controller = null;
                yield break;
            }

            // Connect to the controller now
            try
            {
                _controller.Connect();

                // Update the internal service state
                _state.IsConnected = _controller.IsConnected;
            }
            catch ( Exception ex )
            {
                SoapFault fault = new SoapFault( string.Format( "{0}: Error - failed to connect to the controller.", _controller.Class ), ex );
                LogError( fault.ToString() );
                request.ResponsePort.Post( fault );
                yield break;
            }

            LogInfo( string.Format( "Successfully connected to the {0} controller device on {1}.", _controller.Class, _controller.ComPortName ) );

            request.ResponsePort.Post( new DefaultUpdateResponseType() );
        }

        /// <summary>
        /// Disconnects from the XBee device (if connected).
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [ServiceHandler( ServiceHandlerBehavior.Exclusive )]
        public IEnumerator<ITask> DisconnectHandler( Disconnect request )
        {
            if ( _controller != null )
            {
                if ( _controller.IsConnected )
                {
                    try
                    {
                        _controller.Disconnect();

                        // Update the internal service state
                        _state.IsConnected = _controller.IsConnected;
                    }
                    catch ( Exception ex )
                    {
                        SoapFault fault = new SoapFault( string.Format( "{0}: Error - failed to disconnect from the controller.", _controller.Class ), ex );
                        LogError( fault.ToString() );
                        request.ResponsePort.Post( fault );
                        yield break;
                    }
                }

                _controller.DataSampleReceived -= _dataSampleReceivedHandler;

                LogInfo( string.Format( "Successfully disconnected from the {0} controller device.", _controller.Class ) );

                _controller = null;
            }

            request.ResponsePort.Post( new DefaultUpdateResponseType() );
        }

        /// <summary>
        /// Handles the update message that sends a text string to an xbee end device.
        /// </summary>
        /// <param name="sendMessage"></param>
        /// <returns></returns>
        [ServiceHandler( ServiceHandlerBehavior.Exclusive )]
        public IEnumerator<ITask> DeviceMessageUpdateHandler( DeviceMessageUpdate sendMessage )
        {
            if ( _controller != null )
            {
                try
                {
                    _controller.SendMessageToEndPoint( sendMessage );
                }
                catch ( Exception ex )
                {
                    SoapFault fault = new SoapFault(
                        string.Format( "{0}: Error - failed to send message to device SH=0x{1:x}, SL=0x{2:x}: {3}",
                            _controller.Class,
                            sendMessage.Body.DestinationAddress.AddressHigh,
                            sendMessage.Body.DestinationAddress.AddressLow,
                            ex.Message
                        ), ex );

                    LogError( fault.ToString() );
                    sendMessage.ResponsePort.Post( fault );
                    yield break;
                }
            }
        }

        /// <summary>
        /// Handles Subscribe messages
        /// </summary>
        /// <param name="subscribe">the subscribe request</param>
        [ServiceHandler]
        public void SubscribeHandler( Subscribe subscribe )
        {
            SubscribeHelper( _submgrPort, subscribe.Body, subscribe.ResponsePort );
        }

        /// <summary>
        /// Handles the update message that sends a digital output update message to and xbee device.
        /// </summary>
        /// <param name="sendMessage"></param>
        /// <returns></returns>
        [ServiceHandler( ServiceHandlerBehavior.Exclusive )]
        public IEnumerator<ITask> DeviceOutputUpdateHandler( DeviceOutputUpdate sendMessage )
        {
            if ( _controller != null )
            {
                try
                {
                    _controller.SendDigitalOutputToEndPoint( sendMessage );
                }
                catch ( Exception ex )
                {
                    SoapFault fault = new SoapFault(
                        string.Format( "{0}: Error - failed to send DIO message to device SH=0x{1:x}, SL=0x{2:x}: {3}",
                            _controller.Class,
                            sendMessage.Body.DestinationAddress.AddressHigh,
                            sendMessage.Body.DestinationAddress.AddressLow,
                            ex.Message
                        ), ex );

                    LogError( fault.ToString() );
                    sendMessage.ResponsePort.Post( fault );
                    yield break;
                }
            }
        }

        #endregion

        #region Controller event handlers

        private void _controller_DataSampleReceived( object sender, DataSampleReceivedArgs e )
        {
            // Well known indexes into the received data
            const int supplyVoltIndex = 7;

            DataSample sample = new DataSample
            {
                SourceAddress = e.SourceAddress,
                SourceSerialNumber = e.SourceSerialNumber,
                DigitalChannelMask = e.DigitalChannelMask,
                DigitalIO = e.DigitalIO,
                AnalogChannelMask = e.AnalogChannelMask,
                AnalogIO = e.AnalogIO,
                SupplyVoltage = e.AnalogChannelMask.DIO[ supplyVoltIndex ] ?
                    ( ushort )( e.AnalogIO[ supplyVoltIndex ] * 1200 / 1023 ) : ( ushort )0
            };

            SendNotification( _submgrPort, new DataSampleUpdate( sample ) );
        }

        #endregion
    }
}


